﻿Imports System.Net
Imports System.Net.Sockets
Imports System.Threading
Imports System.Text
Imports System.Runtime.Serialization.Formatters.Binary
Imports System.IO

Public Class frmMain
    Private udpPort As Integer = 65535
    Public chatPort As Integer = 65534
    Public chatroomPort As Integer = 65533

    Private selfHostName As String
    Private selfIP As String
    Private selfDisplayname As String

    'Public Shared mapOfClients As Hashtable
    Public mapOfClients As Hashtable
    Public mapOfChatWindows As Hashtable
    Public mapOfChatroomWindows As Hashtable
    Public Shared mapOfChatroomSockets As Hashtable
    Public alChatroomSockets As ArrayList
    Private mpChatroomIDs As Hashtable ' map of last 3 digits of IPs of participants|ID of open chatroom

    Private ListThread As Thread ' for checking status of clients
    Private chatListenerThread As Thread ' for listening to chat requests
    Private multicastListenerThread As Thread ' for listening to chatroom requests
    Private tcpListener As TcpListener ' for reciving chat messages
    Dim udpListener As UdpClient ' for reciving UDP data when checkin clients' statuses

    Private keepListeningForChatRequests As Boolean = True
    Private keepListeningTOUDP As Boolean = True
    Private keepListeningForChatroomRequests As Boolean = True

    Public Shared mpNumSockUsers As Hashtable

    Delegate Sub UpdateListCallback(ByVal operation As String, ByVal strValue As String)

    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        sysTrayIcon.Visible = False

        Me.Location = My.Settings.WindowPosition
        Me.Size = My.Settings.WindowSize
        Me.selfDisplayname = My.Settings.displayName

        Me.txtDisplayName.Text = Me.selfDisplayname
        If Me.txtDisplayName.Text = "Unknown User" Then
            enableControls(True)
        Else
            enableControls(False)
        End If

        'thread that listens to UDP broadcasts that indicate whoz online
        ListThread = New Thread(New ThreadStart(AddressOf ListeningToUDP))
        ListThread.Priority = ThreadPriority.BelowNormal
        ListThread.IsBackground = True

        'thread that listens to chat requests from other users and initiates a chat window
        chatListenerThread = New Thread(New ThreadStart(AddressOf listeningToChatRequests))
        chatListenerThread.Priority = ThreadPriority.BelowNormal
        ListThread.IsBackground = True

        'thread that listens to chatroom requests from other users and initiates a chatroom window
        multicastListenerThread = New Thread(New ThreadStart(AddressOf listeningToMulticasts))
        multicastListenerThread.Priority = ThreadPriority.BelowNormal
        multicastListenerThread.IsBackground = True

        'start both threads
        ListThread.Start() 'Starts the thread that listens for UDP broadcasts to know about online clients
        chatListenerThread.Start() 'Starts the thread that listens for chat requests
        multicastListenerThread.Start() ' starts the thread that listens for chatroom requests

        'start broadcasting status updates on UDP
        Timer1.Enabled = True

        selfHostName = Dns.GetHostName
        selfIP = GetIPAddress()
        Console.WriteLine("Your Machine Name = " & selfHostName)
        Console.WriteLine("Your IP = " & selfIP)

        mapOfClients = New Hashtable()
        mapOfChatWindows = New Hashtable()
        mapOfChatroomWindows = New Hashtable()
        mapOfChatroomSockets = New Hashtable()
        alChatroomSockets = New ArrayList()
        mpNumSockUsers = New Hashtable()

        My.Application.SaveMySettingsOnExit = True


    End Sub

    Private Sub Form1_FormClosing(ByVal sender As System.Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles MyBase.FormClosing
        'Dim offlinestring As String = " Has gone Offline."
        keepListeningTOUDP = False
        keepListeningForChatRequests = False
        keepListeningForChatroomRequests = False

        'stop broadcasting status updates on UDP
        Timer1.Enabled = False

        My.Settings.WindowPosition = Me.Location
        My.Settings.WindowSize = Me.Size
        My.Settings.Save()

        sendClientStatus("OFFLINE")

        Console.WriteLine("Offline status sent. Invoking END()")

        End
    End Sub

    Private Sub enableControls(ByVal status As Boolean)
        Me.txtDisplayName.Enabled = status
        Me.btSetDisplayName.Enabled = status
        Me.lstOnlineClients.Enabled = Not status
    End Sub

    ' get self IP address
    Public Function GetIPAddress() As String
        Dim oAddr As System.Net.IPAddress
        Dim sAddr As String
        With Dns.GetHostByName(selfHostName)
            oAddr = New System.Net.IPAddress(.AddressList(0).Address)
            sAddr = oAddr.ToString
        End With
        GetIPAddress = sAddr
    End Function

    Private Sub Timer1_Tick(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick
        sendClientStatus("ONLINE")
    End Sub

    'send status to other clients
    Private Sub sendClientStatus(ByVal status As String)
        Try
            Dim broadcastIP As IPAddress = IPAddress.Broadcast
            Dim udpSender As New UdpClient()
            Dim messageBuffer As Byte() = Encoding.ASCII.GetBytes(GetIPAddress() & "|" & My.Settings.displayName & "|" & status) ' get your own IP address
            Dim ep As New Net.IPEndPoint(broadcastIP, udpPort)

            udpSender.EnableBroadcast = True
            udpSender.Send(messageBuffer, messageBuffer.Length, ep)
            udpSender.Close()
        Catch ex As Exception
            Console.WriteLine(ex.ToString)
        End Try
    End Sub

    'The below method listens to broadcasts, to know if a client has come online/gone offline
    Private Sub ListeningToUDP()
        Dim groupEP As New IPEndPoint(IPAddress.Broadcast, udpPort)
        Dim bytes As Byte()
        Dim recievedString As String = ""
        Dim displayName As String = ""
        Dim arrRecievedString(3) As String
        Dim removeIP As String

        Dim frmChatWin As frmChat

        udpListener = New UdpClient(udpPort)
        udpListener.EnableBroadcast = True

        While keepListeningTOUDP
            Try
                bytes = udpListener.Receive(groupEP)
                recievedString = Encoding.ASCII.GetString(bytes, 0, bytes.Length)
                arrRecievedString = recievedString.Split("|")

                displayName = arrRecievedString(1) & "<" & arrRecievedString(0) & ">"

                If Not arrRecievedString(0).Equals(selfIP) Then
                    If arrRecievedString(2).Equals("OFFLINE") Then
                        If (lstOnlineClients.Items.Contains(displayName)) Then
                            updateList("REMOVE", displayName)
                            If mapOfClients.ContainsKey(arrRecievedString(0)) Then
                                mapOfClients.Remove(arrRecievedString(0))

                                If mapOfChatWindows.Contains(arrRecievedString(0)) Then
                                    frmChatWin = mapOfChatWindows.Item(arrRecievedString(0))
                                    frmChatWin.finishChatSession("", "")
                                End If
                            End If
                        End If
                    ElseIf arrRecievedString(2).Equals("ONLINE") Then
                        If Not lstOnlineClients.Items.Contains(displayName) Then
                            updateList("ADD", displayName)
                            If mapOfClients.ContainsKey(arrRecievedString(0)) Then
                                removeIP = arrRecievedString(0)

                                For Each itm In lstOnlineClients.Items
                                    If itm.ToString.IndexOf("<" & removeIP & ">") > 0 Then
                                        updateList("REMOVE", lstOnlineClients.GetItemText(itm))
                                        Exit For
                                    End If
                                Next
                            End If
                            mapOfClients.Remove(arrRecievedString(0))
                            mapOfClients.Add(arrRecievedString(0), arrRecievedString(1))
                        End If
                    End If
                End If
            Catch ex As SocketException
                keepListeningTOUDP = False
                udpListener.Close()
                Console.WriteLine("exception occured in ListeningToUDP ~~ " & ex.ToString())
            End Try
        End While
    End Sub

    Private Sub listeningToChatRequests()
        Dim chatTcpClient As TcpClient
        Dim frmChatWindow As frmChat
        Dim chatIPEndPt As IPEndPoint
        Dim chatRecIP As String

        Try
            tcpListener = New TcpListener(chatPort)
            tcpListener.Start()
            While keepListeningForChatRequests
                'wait for new chat request
                chatTcpClient = tcpListener.AcceptTcpClient()
                chatIPEndPt = chatTcpClient.Client.RemoteEndPoint
                chatRecIP = chatIPEndPt.Address.ToString

                ' a chat window mite be open, with connection severed. Use that window.
                If (mapOfChatWindows.Contains(chatRecIP)) Then
                    frmChatWindow = mapOfChatWindows.Item(chatRecIP)
                    frmChatWindow.chatTcpClient = chatTcpClient
                    frmChatWindow.Show()
                Else
                    'create a new chat window
                    frmChatWindow = New frmChat(chatTcpClient, mapOfClients.Item(chatRecIP), chatRecIP)
                    frmChatWindow.mainFormInstance = Me
                    'maintain list of open chat windows
                    mapOfChatWindows.Add(chatRecIP, frmChatWindow)

                    Application.Run(frmChatWindow)
                End If
                
                frmChatWindow.Focus()
            End While
        Catch ex As Exception
            keepListeningForChatRequests = False
            MessageBox.Show("Exception occured in listeningToChatRequests ~~ " & ex.ToString)
        End Try
    End Sub

    Private Sub listeningToMulticasts()
        Dim CRoomTcpListener As TcpListener
        Dim CRoomTcpClient As TcpClient
        Dim frmChatroomWindow As frmChatroom
        Dim chatIPEndPt As IPEndPoint
        Dim chatRecIP As String
        Dim state As SocketState
        Dim alChatterIPs As ArrayList
        Dim chatRoomID As String

        Try
            CRoomTcpListener = New TcpListener(New IPAddress(Me.selfIP), chatroomPort)
            CRoomTcpListener.Start()
            While keepListeningForChatRequests
                'wait for new chatroom request
                CRoomTcpClient = CRoomTcpListener.AcceptTcpClient()

                ' get the IP of the client
                chatIPEndPt = CRoomTcpClient.Client.RemoteEndPoint
                chatRecIP = chatIPEndPt.Address.ToString

                ' recieve list of chatroom participants to connect to
                alChatterIPs = recieveChattersList(CRoomTcpClient.GetStream())

                ' get or create chatroomId for this chat conversation
                chatRoomID = getChatroomId(alChatterIPs)

                If (mpChatroomIDs.ContainsKey(chatRoomID)) Then
                    frmChatroomWindow = mapOfChatroomWindows.Item(chatRoomID)
                Else
                    state = New SocketState()
                    'set the callback handler to be called when a message arrives
                    CRoomTcpClient.GetStream().BeginRead(state.buffer, 0, state.buffer.Length, New AsyncCallback(AddressOf readChatroomMessage), state)

                    'maintain list of sockets to avoid duplication of sockets between clients
                    mapOfChatroomSockets.Add(chatRecIP, CRoomTcpClient)

                    'create a new chat window
                    frmChatroomWindow = New frmChatroom(CRoomTcpClient, mapOfClients.Item(chatRecIP), chatRecIP, alChatterIPs, chatRoomID)

                    'maintain list of open chat windows
                    mapOfChatroomWindows.Add(chatRoomID, frmChatroomWindow)
                End If

                Application.Run(frmChatroomWindow)
                frmChatroomWindow.Focus()
            End While
        Catch ex As Exception
            keepListeningForChatRequests = False
            MessageBox.Show("Exception occured in recieving chat>> " & ex.ToString)
        End Try
    End Sub

    ' maintain list of number of chatrooms using a socket. Every time a chatroom closes,
    ' decrease the count for the sockets it was using. If any socket has zero count of
    ' windows using it, close that socket.
    Public Shared Sub updateSocketUserCount(ByVal chatterIP As String, ByVal operation As String)
        Dim currCount As Integer = 0
        Dim sck As TcpClient

        If mpNumSockUsers.Contains(chatterIP) Then
            currCount = Convert.ToInt32(mpNumSockUsers.Item(chatterIP))
            mpNumSockUsers.Remove(chatterIP)
        Else
            currCount = 0
        End If

        If operation.Equals("INC") Then
            mpNumSockUsers.Add(chatterIP, "" & (currCount + 1))
        ElseIf operation.Equals("DEC") Then
            If currCount < 1 Then
                MessageBox.Show("EXCEPTION CASE>> Count for ip[" & chatterIP & "] is already zero")
            Else
                If currCount = 1 Then
                    sck = mapOfChatroomSockets.Item(chatterIP)
                    sck.Close()
                Else
                    mpNumSockUsers.Add(chatterIP, "" & (currCount - 1))
                End If
            End If
        End If
    End Sub

    ' asynchronously called to read chatroom message
    Private Sub readChatroomMessage(ByVal ar As IAsyncResult)
        Dim state As SocketState = CType(ar.AsyncState, SocketState)
        Dim stream As NetworkStream = state.tcpClient.GetStream
        Dim numBytesRead As Integer
        Dim formatter As BinaryFormatter = New BinaryFormatter()
        Dim chatRoomMessage As ChatroomMsg

        numBytesRead = stream.EndRead(ar)
        If (numBytesRead > 0) Then
            chatRoomMessage = formatter.Deserialize(New MemoryStream(state.buffer))
            Console.WriteLine("Chatroom message recieved from <{0}> for [{1}]: {2}", chatRoomMessage.getSenderName, chatRoomMessage.getRecieverName, chatRoomMessage.getMessage)
        End If

        'now set the async handler for the next read on the same socket
        state = New SocketState()
        stream.BeginRead(state.buffer, 0, state.buffer.Length, New AsyncCallback(AddressOf readChatroomMessage), state)
    End Sub

    'update the list of online/offline clients
    Private Sub updateList(ByVal operation As String, ByVal strValue As String)
        If lstOnlineClients.InvokeRequired Then
            Dim d As New UpdateListCallback(AddressOf updateList)
            Me.Invoke(d, New Object() {operation, strValue})
        Else
            If operation.Equals("ADD") Then
                lstOnlineClients.Items.Add(strValue)
            ElseIf operation.Equals("REMOVE") Then
                lstOnlineClients.Items.Remove(strValue)
            End If
        End If
    End Sub

    Private Sub btSetDisplayName_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btSetDisplayName.Click
        Me.selfDisplayname = Me.txtDisplayName.Text
        My.Settings.displayName = Me.txtDisplayName.Text
        enableControls(False)
    End Sub

    Private Sub lstOnlineClients_dblClicked(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lstOnlineClients.DoubleClick
        Dim chatWindow As frmChat
        Dim selectedRecipient As String
        Dim chatRecIP As String
        Dim chatRecName As String
        Dim chatTcpClient As TcpClient

        If lstOnlineClients.SelectedItems.Count = 1 Then
            selectedRecipient = lstOnlineClients.SelectedItem.ToString
            chatRecName = selectedRecipient.Substring(0, selectedRecipient.IndexOf("<"))
            chatRecIP = selectedRecipient.Substring(selectedRecipient.IndexOf("<") + 1).TrimEnd(">")

            If mapOfChatWindows.ContainsKey(chatRecIP) Then
                'chat window with this user is already open so simply show it
                chatWindow = mapOfChatWindows.Item(chatRecIP)
                chatWindow.Show()
                chatWindow.Focus()
            Else
                Try
                    'create new chat window with user and show
                    chatTcpClient = New TcpClient(chatRecIP, chatPort)
                    chatWindow = New frmChat(chatTcpClient, chatRecName, chatRecIP)
                    mapOfChatWindows.Add(chatRecIP, chatWindow)
                    chatWindow.mainFormInstance = Me

                    chatWindow.Show()
                    chatWindow.Focus()
                Catch ex As Exception
                    Console.WriteLine(" error in lstOnlineClients_dblClicked ~~ " & ex.ToString)
                End Try
            End If
            
        Else
            MessageBox.Show("Please selected a single person to chat with,\n or use MC button to open chatroom")
        End If
    End Sub

    Private Sub form_hide(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Resize
        If Me.WindowState = FormWindowState.Minimized Then
            sysTrayIcon.Visible = True
            Me.Visible = False
        End If
    End Sub

    Private Sub sysTrayIcon_MouseClick(ByVal sender As System.Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles sysTrayIcon.MouseClick
        Me.Visible = True
        Me.WindowState = FormWindowState.Normal
        sysTrayIcon.Visible = False
    End Sub

    ' check if the chatters mentioned are already sharing a chatroom. If yes, return
    ' tat chatroom's id. Else create a new chatroomId and return
    Private Function getChatroomId(ByVal alChatterIPs As ArrayList)
        'step1: take last 3 digits of the IPs of all chatters & order them
        Dim chatterIP As String
        Dim lastSection As String
        Dim alOrderedIps As ArrayList = New ArrayList()
        Dim finalString As String = ""
        For Each chatterIP In alChatterIPs
            lastSection = chatterIP.Substring(chatterIP.LastIndexOf(":") + 1)
            alOrderedIps.Add(lastSection)
        Next
        alOrderedIps.Sort()
        For Each strng In alOrderedIps
            finalString += strng + "|"
        Next
        finalString = finalString.Substring(0, finalString.Length - 1) ' remove the last | character
        If Not (mpChatroomIDs.Contains(finalString)) Then
            mpChatroomIDs.Add(finalString, alChatterIPs)
        End If

        Return finalString
    End Function

    ' this is called the first time a message is recieved over the new chatroom request socket
    ' a ChatroomMsg object is expected containing list of participating chatters.
    ' deserialize this object and pass back the list of chatters.
    Private Function recieveChattersList(ByRef stream As NetworkStream)
        Dim msg As ChatroomMsg
        Dim buffer(1024) As Byte
        stream.Read(buffer, 0, buffer.Length)

        msg = New BinaryFormatter().Deserialize(New MemoryStream(buffer))
        Return msg.getListOfChatterIPs()
    End Function

    'Public Shared Function getClientsData()
    '    Return frmMain.mapOfClients
    'End Function

    ' if multiple users are selected, enable the MC(multicast) button
    Private Sub lstOnlineClients_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lstOnlineClients.SelectedIndexChanged
        If lstOnlineClients.SelectedIndices.Count > 1 Then
            Me.btnMC.Enabled = True
        Else
            Me.btnMC.Enabled = False
        End If
    End Sub

    ' initiate a multicast chatroom
    ' 1. get list of chatters
    ' 2. check if a chatroom is open for this list. if not initiate one
    Private Sub btnMC_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnMC.Click
        Dim alChatterIPs As ArrayList = New ArrayList
        Dim chatterIP As String
        Dim chatroomID As String
        Dim frmChatroomwindow As frmChatroom
        Dim tcpClnt As TcpClient

        ' get list of chatters to chat with
        For Each itm In lstOnlineClients.SelectedItems
            chatterIP = itm.ToString.Substring(itm.ToString.IndexOf("<") + 1) ' get the IP part of the username
            chatterIP = chatterIP.Substring(0, chatterIP.Length - 2) ' remove the last ">"
            alChatterIPs.Add(chatterIP)
        Next

        ' get chatroom id
        chatroomID = getChatroomId(alChatterIPs)

        ' if room exists, show it; else, create one
        If mapOfChatroomWindows.Contains(chatroomID) Then
            frmChatroomwindow = mapOfChatroomWindows.Item(chatroomID)
        Else
            ' check if chatroom sockets exist to the chatters. else create them
            For Each chatterIP In alChatterIPs
                If Not mapOfChatroomSockets.Contains(chatterIP) Then
                    ' connect to chatter on chatroom socket 
                    tcpClnt = New TcpClient(chatterIP, chatroomPort)
                    ' add this open socket to the map maintained in frmMain to
                    ' avoid creation of duplicate socket on further requests
                    mapOfChatroomSockets.Add(chatterIP, tcpClnt)
                End If
            Next
            frmChatroomwindow = New frmChatroom(tcpClnt, mapOfClients.Item(chatterIP), chatterIP, alChatterIPs, chatroomID)
        End If

        'show the chatroom
        frmChatroomwindow.Show()
        frmChatroomwindow.Focus()
    End Sub
End Class
